Vercel Chat SDK 是什么

Vercel Chat SDK 是一个面向多平台的聊天机器人开发框架,提供统一的 Channel、Thread、Message 抽象。基于该 SDK,开发者可以用同一套业务代码同时支持 Slack、Microsoft Teams、Discord、Telegram、WhatsApp、GitHub、Linear 等多个 IM 与协作平台。

它的核心理念是 Channel 抽象:把平台特定的鉴权、消息收发、状态管理统一封装在适配器层之下,业务代码只关心 Channel/ Thread/ Message。新平台的接入由对应的"适配器(adapter)"承担。

飞书官方适配器

飞书已经为 Vercel Chat SDK 发布了官方适配器 @larksuite/vercel-chat-adapter,让 Chat SDK 上构建的产品可以无缝接入飞书。

适配器支持的能力:

  • 原生 cardkit 卡片流式(typewriter,非 post + edit 轮询)
  • 完整交互卡片(按钮、链接、菜单、字段、图片)
  • 消息编辑 / 删除 / 表情回复
  • 历史消息拉取(覆盖飞书 20+ 种原生消息类型)
  • 群聊 / 私聊识别 及 @ 提及处理
  • WebSocket 长连接传输
  • registerLarkApp 扫码授权一键创建应用

这个适配器基于飞书 Channel SDK 构建(详见下方适配器实现原理章节)。

快速开始

创建飞书应用

方式一:扫码授权(推荐)

调用 registerLarkApp 触发飞书官方扫码流程,生成一次性链接,用户扫码后自动返回 client_id / client_secret,并预配好本适配器所需的权限与事件订阅。

import { registerLarkApp } from "@larksuite/vercel-chat-adapter";
import qrcode from "qrcode-terminal"; // pnpm add -D qrcode-terminal

const { client_id, client_secret } = await registerLarkApp({
  onQRCodeReady: ({ url }) => {
    console.log("使用飞书 / Lark 手机端扫码:");
    qrcode.generate(url, { small: true });
  },
  onStatusChange: ({ status }) => console.log("授权状态:", status),
});

console.log("LARK_APP_ID=", client_id);
console.log("LARK_APP_SECRET=", client_secret);

只需运行一次。把返回的 client_id / client_secret 持久化保存,后续启动通过 LARK_APP_ID / LARK_APP_SECRET 环境变量传入即可。

方式二:开发者后台手动创建

在 open.larksuite.com/app(Lark)或 open.feishu.cn/app(飞书)创建应用,获取 client_id / client_secret。

安装适配器

pnpm add @larksuite/vercel-chat-adapter chat @chat-adapter/state-memory

最小可运行示例

import { Chat } from "chat";
import { createMemoryState } from "@chat-adapter/state-memory";
import { createLarkAdapter } from "@larksuite/vercel-chat-adapter";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    lark: createLarkAdapter({
      appId: process.env.LARK_APP_ID!,
      appSecret: process.env.LARK_APP_SECRET!,
    }),
  },
  state: createMemoryState(),
});

bot.onNewMention(async (thread, message) => {
  await thread.subscribe();
  await thread.post(`收到:${message.text}`);
});

bot.onDirectMessage(async (thread, message) => {
  await thread.post(`收到私信:${message.text}`);
});

await bot.initialize();

bot.initialize() 会建立 WebSocket 长连接并保持运行,Node.js 进程会一直运行直到 bot.shutdown() 被调用。

配置环境变量

如果不显式传 appId、appSecret,适配器会从以下环境变量自动读取:

  • LARK_APP_ID —— 飞书 App ID
  • LARK_APP_SECRET —— 飞书 App Secret

适配器实现原理

这个适配器基于 飞书 Channel SDK(@larksuiteoapi/node-sdk 中的 LarkChannel)构建。Channel SDK 是飞书提供的高阶 SDK,封装了:

  • 事件订阅与签名验证
  • WebSocket 长连接与心跳重连
  • 20+ 种原生消息类型的归一化(normalize() 输出统一格式)
  • Cardkit 卡片流式接口
  • 历史消息拉取
  • 应用凭证管理(自动 token 刷新)

适配器开发者只需要做"接口翻译" —— 把 Channel SDK 的接口映射到 Vercel Chat SDK 的 Adapter 接口。下面是核心代码示意(简化版):

import { LarkChannel } from "@larksuiteoapi/node-sdk";
import type { Adapter, Message } from "chat";

export const createLarkAdapter = (config?: LarkConfig): Adapter => {
  const channel = new LarkChannel({
    appId: config?.appId ?? process.env.LARK_APP_ID,
    appSecret: config?.appSecret ?? process.env.LARK_APP_SECRET,
    safety: { enabled: false }, // Chat SDK 已处理 dedupe / lock
  });

return {
    name: "lark",

async initialize() {
      // 收消息:Channel SDK 已经归一化,适配器只做格式映射
      channel.on("message", (normalized) => {
        const threadId = encodeThreadId(normalized.chatId, normalized.rootId);
        chat.handleIncomingMessage({
          text: normalized.text,
          threadId,
          author: {
            id: normalized.senderId,
            isMe: normalized.senderId === botIdentity.openId,
          },
          raw: normalized,
        });
      });
      await channel.connect();
    },

async post(threadId, message: Message) {
      const { chatId, rootId } = decodeThreadId(threadId);
      return channel.send({
        chatId,
        content: renderMarkdown(message),
        replyTo: rootId,
      });
    },

async stream(threadId, handler) {
      const { chatId, rootId } = decodeThreadId(threadId);
      // 使用原生 cardkit typewriter,非 post + edit 轮询
      return channel.cardkit.stream({
        chatId,
        replyTo: rootId,
        produce: handler,
      });
    },

async fetchMessages(threadId) {
      const { chatId, rootId } = decodeThreadId(threadId);
      const history = await channel.getHistoricalMessages({ chatId, rootId });
      return history.map(toMessage);
    },
  };
};

整个适配器的核心控制流不到百行 —— Channel SDK 已经处理了大部分飞书平台特定的复杂度,适配器只承担"接口翻译"这一核心工作。

相关链接